﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AutoMerge.Properties;

namespace AutoMerge
{
    /// <summary>
    /// Служба слияния изменений
    /// </summary>
    internal class DefaultMergeService : IMergeService
    {
        /// <summary>
        /// Объеденить изменения внесенные в файл
        /// </summary>
        /// <param name="logService">Внешняя служба протоколирования</param>
        /// <param name="resultFileName">Имя выходного файла</param>
        /// <param name="sourceFileName">Имя файла с исходным текстом</param>
        /// <param name="changedFileNames">Имена файлов с измененными текстами</param>
        /// <returns>Список выявленных изменений сгруппированных по именам файлов-источников</returns>
        public IEnumerable<IGrouping<string, ModificationMarker>> MergeChangedFiles(ILogService logService, 
            string resultFileName, string sourceFileName, params string[] changedFileNames)
        {

            #region Проверка входных параметров

            if (logService == null)
                throw new ArgumentNullException("logService");
            if (string.IsNullOrWhiteSpace(resultFileName))
                throw new ArgumentNullException("resultFileName");
            if (string.IsNullOrWhiteSpace(sourceFileName))
                throw new ArgumentNullException("sourceFileName");
            if (!File.Exists(sourceFileName))
                throw new FileNotFoundException("", sourceFileName);

            var notFoundFiles = from c in changedFileNames where !File.Exists(c) select c;
            if (notFoundFiles.Count() > 0)
                throw new FileNotFoundException(string.Format(Resources.ctrChangedFileNotFound, 
                    (notFoundFiles.Count() > 1 ? Resources.strMultiSuffix : ""), string.Join(",", notFoundFiles))); 

            #endregion

            logService.AddLogRecord(Resources.strReadInputFiles);

            // читаем содержимое исходного файла
            var sourceLines = (from c in File.ReadAllLines(sourceFileName) select c).ToList();
            logService.AddLogRecord(Resources.strLinesReadedFromSource, sourceLines.Count, sourceFileName);

            // читаем измененные файлы
            var changedFiles = new Dictionary<string, List<string>>();
            changedFileNames.AsParallel().Select(f => readChangedFile(logService, changedFiles, f)).ToList();

            // поиск изменений
            var changesGroups = changedFiles.AsParallel().Select(x => 
                detectFileChanges(logService, x.Key, sourceLines, x.Value));

            // объединяем изменения в один список
            var changes = new List<ModificationMarker>();
            changesGroups.ToList().ForEach(changes.AddRange);
            changes.Sort();

            if (changes.Count > 0)
            {

#if DEBUG

                File.WriteAllLines(Path.GetDirectoryName(resultFileName) + @"\ChangesProtocol.txt",
                    (from c in changes select c.ToString()).ToList());
                
#endif
                // выборка списка конфликтов
                logService.AddLogRecord(Resources.strConflictDetection);
                var conflicts = selectConflicts(changes);
                if(conflicts.Count > 0)
                    logService.AddLogRecord(Resources.strConflictsFound, conflicts.Count);
                // формируем собержимое выходного файла
                logService.AddLogRecord(Resources.strConstructingOutput);
                var outputLines = constructOutput(sourceLines, changes, conflicts);
                // пишем выходной файл
                File.WriteAllLines(resultFileName, outputLines);
            }
            else
            {
                // изменения не найдены
                logService.AddLogRecord(Resources.strNoChanges, sourceFileName);
                File.WriteAllLines(resultFileName, sourceLines);
            }
            return (from c in changes group c by c.FileName into g select g);
        }

        /// <summary>
        /// Слияние измененных текстов
        /// </summary>
        /// <param name="resultText">Результирующий текст</param>
        /// <param name="sourceText">Исходный текст</param>
        /// <param name="changedTexts">Измененные тексты</param>
        /// <returns></returns>
        public void MergeChangedTexts(out string[] resultText, string[] sourceText, params string[][] changedTexts)
        {
            var resultFileName = Path.GetTempFileName();
            var sourceFileName = Path.GetTempFileName();
            File.WriteAllLines(sourceFileName, sourceText);
            var changedFileNames = new List<string>();
            foreach (var changedText in changedTexts)
            {
                var fileName = Path.GetTempFileName();
                File.WriteAllLines(fileName, changedText);
                changedFileNames.Add(fileName);
            }

            var result = MergeChangedFiles(new LogServiceStub(), resultFileName, 
                sourceFileName, changedFileNames.ToArray());

            resultText = File.ReadAllLines(resultFileName);
            changedFileNames.AddRange(new[] {resultFileName, sourceFileName});
            changedFileNames.ForEach(File.Delete);
        }


        /// <summary>
        /// Сформировать содержимое выходного файла
        /// </summary>
        /// <param name="sourceLines">Строки входного файла</param>
        /// <param name="changes">Массив маркеров изменений</param>
        /// <param name="conflicts">Массив конфликтов</param>
        /// <returns>Содержимое выходного файла</returns>
        private static List<string> constructOutput(List<string> sourceLines, 
            List<ModificationMarker> changes, 
            List<ModificationMarker> conflicts)
        {
            var result = new List<string>();
            var i = 0;
            // массив пронумерованных строк исходного файла
            var source = (from c in sourceLines select c).ToDictionary(x => i++);
            foreach (var sourceIndex in source.Keys)
            {
                // текущая строка исходного файла
                var sourceLine = source[sourceIndex];
                var sind = sourceIndex;
                // маркеры изменений для текущей строки
                var lineChanges = (from c in changes where c.SourceRowNum == sind && !c.Conflict 
                                   orderby c.OrderIndex select c).ToList();

                // обработка маркеров вставки
                var instList = (from c in lineChanges
                                where c is InsertMarker
                                orderby c.OrderIndex
                                select c as InsertMarker).Distinct(new EqComparerAdd()).ToList();
                instList.ToList().ForEach(y => { result.Add(y.SubjectString); y.ResultRowNum = result.Count - 1; });

                // типы изменений найденных для текущей строки
                var types = (from c in lineChanges select c.GetType()).Distinct().ToList();
                if(!types.Contains(typeof(DeleteMarker)))
                {
                    // если текущая строка исходного файла должна попасть в выходной файл
                    var destLine = sourceLine;

                    if (types.Contains(typeof(ChangeMarker)))
                    {
                        // если строка изменена, то обрабатываем маркер изменения (должен быть единственный)
                        var changeMarker = (from c in lineChanges
                                    where c.GetType() == typeof (ChangeMarker)
                                    select c).Cast<ChangeMarker>().FirstOrDefault();
                        destLine = changeMarker.SubjectString;
                        changeMarker.ResultRowNum = result.Count;
                    }
                    result.Add(destLine);
                }

                // обработка маркеров добавления
                var appendList = (from c in lineChanges
                               where c is AppendMarker
                               orderby c.OrderIndex
                               select c as AppendMarker).Distinct(new EqComparerAdd());
                appendList.ToList().ForEach(y => { result.Add(y.SubjectString); y.ResultRowNum = result.Count - 1;});

                // выводим информацию о конфликтах для текущей строки
                var currentLineConflicts = from c in conflicts where c.SourceRowNum == sind select c;
                if (currentLineConflicts.Count() <= 0) continue;

                result.Add("/*");
                result.Add(string.Format(Resources.strMergeConflict, sind+1));
                currentLineConflicts.ToList().ForEach(x => result.Add(x.ToString()));
                result.Add("*/");
            }
            return result;
        }

        /// <summary>
        /// Выборка списка конфликтов
        /// </summary>
        /// <param name="changes">Общий список маркеров изменений</param>
        /// <returns>Список конфликтов</returns>
        private static List<ModificationMarker> selectConflicts(List<ModificationMarker> changes)
        {
            var result = new List<ModificationMarker>();
            // группируем маркеры изменений по номерам строк исходного файла
            var overlaps = (from c in changes group c by c.SourceRowNum into g select g).ToList();
            // выборка конфликтов для каждой из исходных строк
            var conflicts = overlaps.Select(selectIncompartibleMarkers);
            // сливаем все конфликтные маркеры в кучу
            conflicts.ToList().ForEach(result.AddRange);
            result.ForEach(x=>x.Conflict = true);
            return result;
        }

        /// <summary>
        /// Выборка несовместимых маркеров изменений для одной строки исходного текста
        /// </summary>
        /// <param name="markers">Список маркеров изменений для строки</param>
        /// <returns></returns>
        private static IEnumerable<ModificationMarker> selectIncompartibleMarkers(IEnumerable<ModificationMarker> markers)
        {
            if (markers == null) throw new ArgumentNullException("markers");

            var result = new HashSet<ModificationMarker>();
            // изменение строки в нескольких версиях
            var changeMarkers = (from c in markers where c is ChangeMarker select c).ToList();
            if (changeMarkers.Count() > 1)
                changeMarkers.ForEach(y => result.Add(y));

            // одновременное изменение и удаление строки в нескольких версиях
            var deleteMarkers = (from c in markers where c is DeleteMarker select c).ToList();
            if(deleteMarkers.Count>0 && changeMarkers.Count>0)
            {
                changeMarkers.Cast<ModificationMarker>().Union(
                deleteMarkers.Cast<ModificationMarker>()).ToList().ForEach(x => result.Add(x));
            }
            return result;
        }

        /// <summary>
        /// Поиск изменений (исполняется в отдельном потоке).
        /// Реализация алгоритма LCS (см. http://rickosborne.org/blog/2006/09/difflcs-screencast-how-difference-engines-work/)
        /// </summary>
        /// <param name="logService">Внешняя служба протоколирования</param>
        /// <param name="changedFileName">Имя измененного файла</param>
        /// <param name="sourceLines">Строки исходного файла</param>
        /// <param name="changedLines">Строки измененного файла</param>
        /// <returns></returns>
        private static List<ModificationMarker> detectFileChanges(ILogService logService, string changedFileName, 
            IEnumerable<string> sourceLines, 
            IEnumerable<string> changedLines)
        {
            logService.AddLogRecord(Resources.strSearchChanges, changedFileName);

            // локальные копии содержимого файлов
            var source = sourceLines.ToList();
            source.Add("");
            var changed = changedLines.ToList();
            changed.Add("");
            var result = new List<ModificationMarker>();

            var startPosition = 0;
            // исключаем из рассмотрения совпадающие строки в начале файлов
            while ((source.Count > 0 && changed.Count > 0) 
                && (source[0].Trim() == changed[0].Trim()))
            {
                source.RemoveAt(0);
                changed.RemoveAt(0);
                startPosition++;
            }
            // выход при полном совпадении файлов
            if (source.Count == 0 && changed.Count == 0) return result;

            // расчет хешей (для ускорения работы с матрицей лучше использовать хеши вместо строк)
            var sourceHash = (from c in source select c.GetHashCode()).ToList();
            var changedHash = (from c in changed select c.GetHashCode()).ToList();

            var sourceLen = sourceHash.Count;
            var changedLen = changedHash.Count;

            // заполняем матрицу маршрута изменений
            // строки матрицы - хеши измененных строк
            // столбцы матрицы - хеши исходных строк
            var routeMatrix = new int[changedLen,sourceLen];
            for (var i = changedLen - 1; i >= 0; i--)
                for (var j = sourceLen - 1; j >= 0; j--)
                {
                    var downValue = i == changedLen - 1 ? 0 : routeMatrix[i + 1, j];
                    var rightValue = j == sourceLen - 1 ? 0 : routeMatrix[i, j + 1];
                    routeMatrix[i, j] = Math.Max(downValue, rightValue);
                    if (changedHash[i] == sourceHash[j])
                        routeMatrix[i, j]++;
                }

            // определение изменений
            var changedRowIndex = 0;
            var sourceRowIndex = 0;
            var sourceDone = false;
            var changesDone = false;
            while (true)
            {
                if (sourceHash[sourceRowIndex] == changedHash[changedRowIndex])
                {
                    // если текущее исходное и измененное значения совпадают
                    // то переходим по диагонали вниз и вправо
                    changedRowIndex++;
                    sourceRowIndex++;
                }
                else
                {

                    #region Поиск последовательной группы изменений (дает скорее негативный результат при накладывающихся изменениях)

                    //// поиск на текущей диагонали одинаковых строк
                    //var changedCandidates = new List<ChangeMarker>();
                    //var diagRow = changedRowIndex;
                    //var diagCol = sourceRowIndex;
                    //while ((diagRow < changedLen && diagCol < sourceLen) 
                    //    && (sourceHash[diagCol] != changedHash[diagRow]))
                    //{
                    //    changedCandidates.Add(new ChangeMarker(diagCol + startPosition, changedFileName, changed[diagRow]));
                    //    diagCol++;
                    //    diagRow++;
                    //}

                    //if (diagRow < changedLen && diagCol < sourceLen)
                    //{
                    //    changedRowIndex = diagRow+1;
                    //    sourceRowIndex = diagCol+1;
                    //    result.AddRange(changedCandidates);
                    //}
                    //else

                    #endregion

                    {
                        // обработка по исходному алгоритму
                        var rightValue = sourceRowIndex == sourceLen - 1
                                             ? 0
                                             : routeMatrix[changedRowIndex, sourceRowIndex + 1];

                        var bottomValue = changedRowIndex == changedLen - 1
                                              ? 0
                                              : routeMatrix[changedRowIndex + 1, sourceRowIndex];

                        if (rightValue > bottomValue || changesDone)
                        {
                            // значение в матрице маршрута справа от текущего 
                            // превышает значение находящееся снизу от текущего
                            // регистрируем удаление строки в исходном файле
                            // и двигаемся вправо по матрице
                            result.Add(new DeleteMarker(sourceRowIndex + startPosition,
                                                        changedFileName, source[sourceRowIndex]
                                                        , -1));
                            sourceRowIndex++;
                        }
                        else
                        {
                            // иначе регистрируем вставку строки
                            // и двигаемся вниз по матрице
                            var insMarker = new InsertMarker(sourceRowIndex + startPosition,
                                changedFileName, changed[changedRowIndex], 
                                changedRowIndex + startPosition);
                            result.Add(insMarker);
                            changedRowIndex++;
                        }
                    }
                }

                if (sourceRowIndex >= sourceLen)
                    sourceDone = true;

                if (changedRowIndex >= changedLen)
                    changesDone = true;

                if (sourceDone && changesDone) break;

                sourceRowIndex = sourceRowIndex >= sourceLen ? sourceLen - 1 : sourceRowIndex;
                changedRowIndex = changedRowIndex >= changedLen ? changedLen - 1 : changedRowIndex;
            }

            // замещаем последовательную пару маркеров Insert+Delete на Change
            var resultIndex = 0;
            while (resultIndex < result.Count - 1)
            {
                var currentMarker = result[resultIndex];
                var nextMarker = result[resultIndex + 1];
                if ((currentMarker.SourceRowNum == nextMarker.SourceRowNum)
                    && (currentMarker is InsertMarker && nextMarker is DeleteMarker))
                {
                    result[resultIndex] = new ChangeMarker(currentMarker.SourceRowNum,
                                                           currentMarker.FileName,
                                                           currentMarker.SubjectString,
                                                           currentMarker.VersionRowNum) {OrderIndex = 0};
                    result.RemoveAt(resultIndex + 1);
                }
                resultIndex++;
            }

            // замещаем маркеры Insert на Append для последней строки
            var lastRowInsMarkers = (from c in result 
                                     where c is InsertMarker && c.SourceRowNum == (startPosition + sourceLen - 1) 
                                     select c as InsertMarker).ToList();

            foreach (var insMarker in lastRowInsMarkers)
            {
                var markerIndex = result.IndexOf(insMarker);
                var appendMarker = new AppendMarker(insMarker.SourceRowNum - 1, insMarker.FileName, 
                    insMarker.SubjectString, insMarker.VersionRowNum);
                result[markerIndex] = appendMarker;
            }

            return result;
        }

        /// <summary>
        /// Чтение измененного файла (исполняется в отдельном потоке)
        /// </summary>
        /// <param name="logService">Внешняя служба протоколирования</param>
        /// <param name="changedFiles">Словарь измененных файлов для пополнения</param>
        /// <param name="fileName">Имя измененного файла</param>
        /// <returns></returns>
        private static string readChangedFile(ILogService logService, 
            Dictionary<string, List<string>> changedFiles, string fileName)
        {
            var lines = File.ReadAllLines(fileName).ToList();
            lock(changedFiles)
                changedFiles.Add(fileName, lines);
            logService.AddLogRecord(Resources.strLinesReadedFromChanged, lines.Count, fileName);
            return fileName;
        }

    }
}
